An Introduction to Shiny

A hands-on workshop

Andreas M. Brandmaier and Leonie Hagitte

2024-03-14

Herzlich Wilkommen!

Ask questions anytime

Shiny

Shiny is an R package that makes it easy to build interactive web apps in R

Apps can be

  • standalone,
  • deployed to a website,
  • or be part of an interactive (Markdown) document

How do Shiny apps look like?

Some examples:

Required software

You need to install these software packages in order to follow along with the examples of today:

And a couple of R packages:

  • shiny, tidyverse packages, palmerpenguins, …
install.packages(c("shiny","tidyverse", "shinydashboard","palmerpenguins"))

Workshop materials

Please find the slides and code snippets here:

https://github.com/brandmaier/shiny_workshop_2024

What to expect

  • This is a hands-on workshop; you’ll get the most out of it if you download the materials and actively participate
  • Introductory R coding skills are OK! We have exercises at varying levels of proficiency
  • The workshop materials remain open and accessible after the workshop
  • Feel free to team up!

Goals

Objectives of today

  • Learn about the structure of a shiny application.

  • Learn how to create shiny apps from a template.

  • Learn how to think in terms of inputs and outputs.

  • Write your own apps (using simulated data, real data or your data)

Content

Let’s talk about…

  • User-interface / Layout
  • Reactivity / Logic
  • Awesome visualizations

Anatomy of a Shiny app

library(shiny)

shinyApp(
  ui = list(),
  server = function(input, output, session) {  }
)

We first load the shiny package and define a shinyApp, which really is only a function call with two arguments.

Anatomy of a Shiny app

library(shiny)

shinyApp(
  ui = list(),
  server = function(input, output, session) {  }
)

The ui specifies the visible user interface

  • Dynamic elements inputs and outputs
  • Static elements like headings, text, static images
  • A layout how to arrange these things

Anatomy of a Shiny app

library(shiny)

shinyApp(
  ui = list(),
  server = function(input, output, session) {  }
)

The server is invisible and is responsible for all computations

  • The server monitors inputs
  • When inputs change, outputs are updated (reactivity)

User-interface

Example

Inputs have unique ids that correspond to server-side variables, a label, a starting value and extra options (e.g., range restrictions, etc.)

textInput(inputId="familyname", label="Family name:", value="Steve Miller" )

or

numericInput(inputId="age", label="Age (in years):", value=1, min=0, max=150 )

On the server, we will be able to access variables input$familyname and input$age

Layout

Multi-row layout

Other layouts

Many more, e.g. Tabsets - see tabsetPanel()

Outputs

Example output elements (placeholders for dynamic content):

  • textOutput() or htmlOutput()
  • plotOutput()
  • tableOutput()

You can use

help.search("Output", package = "shiny")

to find other output functions in shiny.

Outputs and Renderers

Each *Output() function has a corresponding render*() server-side function. For example:

  • textOutput() \(\rightarrow\) renderText()
  • plotOutput() \(\rightarrow\) renderPlot()
  • tableOutput() \(\rightarrow\) renderTable()

Server logic: Accessing inputs

shinyApp(
  ui = list(),
  server = function(input, output, session) {  }
)
  • Inputs are accessed in the server function via the input argument.

  • Inputs are reactive, meaning that changes trigger updates to outputs.

Example: A pocket calculator

Demo 1 - Plus One

Demo: We write a simple calculator that adds +1 to a number we enter.

The simplest structure of a reactive program involves just a source and an endpoint:

flowchart LR
  subgraph outputs
  re([result])
  end
  subgraph inputs
  n1([number]) 
  end
  n1 --> re

Demo 1 - Plus One

R/demo1.R

library(shiny)

# Define UI for application that draws a histogram
ui <- fluidPage(
  
  # Application title
  titlePanel("Calculator"),
  
  # Sidebar with a slider input for number of bins 
  sidebarLayout(
    sidebarPanel(
      numericInput("number",
                   "Number", value=0)
    ),
    
    # Show a plot of the generated distribution
    mainPanel(
      h3("Result"),
      textOutput("result")
    )
  )
)

# Define server logic required to draw a histogram
server <- function(input, output) {
  
  output$result <- renderText({
    return(input$number + 1)
  })
}

# Run the application 
shinyApp(ui = ui, server = server)

Seeking AI help

Large language models are great companions for programming

Here is a ChatGPT link (requires Microsoft or Google account) to answer your questions (but please ask us as well any time)

ChatGPT companion for Shiny

Your turn - Exercise 1

Copy the code from the previous slide (or open R/demo1.R) and run it in R

Check that you are able successfully run the shiny app and are able to interact with it.

  • If everything is working try modifying the code (e.g. try adding a second number input and change the logic so that both numbers are added).

Reactive diagram

The reactive diagram of this solution shows two inputs and one output:

flowchart LR
  subgraph outputs
  re([result])
  end
  subgraph inputs
  n1([number1]) 
  n2([number2]) 
  end
  n1 --> re
  n2 --> re

Solution

R/solution1_1.R

library(shiny)

# Define UI for application that draws a histogram
ui <- fluidPage(

    # Application title
    titlePanel("Calculator"),

    # Sidebar with a slider input for number of bins 
    sidebarLayout(
        sidebarPanel(
            numericInput("n1",
                        "Number", value=0),
            numericInput("n2",
                         "Number", value=0)
        ),

        # Show a plot of the generated distribution
        mainPanel(
           textOutput("result")
        )
    )
)

# Define server logic required to draw a histogram
server <- function(input, output) {

    output$result <- renderText({
        return(input$n1+input$n2)
    })
}

# Run the application 
shinyApp(ui = ui, server = server)

Your Turn - Exercise 2

  • Continue with your code (or from R/solution1_1.R) and add a menu to choose different operators (e.g., plus, minus, …)

  • For example, add a selectInput(inputId, label, choices)

  • Add server-side logic to implement the different operators

Reactive diagram

flowchart LR
  subgraph outputs
  re([result])
  end
  subgraph inputs
  n1([number1]) 
  n2([number2]) 
  op([operator])
  end
  n1 --> re
  n2 --> re
  op --> re

Solution

R/solution1_2.R

library(shiny)

# Define UI for application that draws a histogram
ui <- fluidPage(

    # Application title
    titlePanel("Calculator"),

    # Sidebar with a slider input for number of bins 
    sidebarLayout(
        sidebarPanel(
            numericInput("n1",
                        "Number", value=0),
            numericInput("n2",
                         "Number", value=0),
            selectInput("operator","Operator",c("+","-","/","*"))
        ),

        # Show a plot of the generated distribution
        mainPanel(
           textOutput("result")
        )
    )
)

# Define server logic required to draw a histogram
server <- function(input, output) {

    output$result <- renderText({
       result <- switch (input$operator,
          "+" = input$n1+input$n2,
          "-" = input$n1-input$n2,
          "/" = input$n1/input$n2,
          "*" = input$n1*input$n2
        )
        return(result)
    })
}

# Run the application 
shinyApp(ui = ui, server = server)

Formatting text

We can use HTML elements to style text. E.g.,

<b>Bold</b> or <i>Italics</i>,h1>First-level heading</h> <h2>Second-level heading</h2>, ...

In UI as static or dynamic elements:

    h2("Title"),
    htmlOutput(outputId = "result")

On the server:

output$result <- renderText({ "<h2>Headline</h2>" })

Solution

R/solution1_3.R

library(shiny)

# Define UI for application that draws a histogram
ui <- fluidPage(

    # Application title
    titlePanel("Calculator"),

    # Sidebar with a slider input for number of bins 
    sidebarLayout(
        sidebarPanel(
            numericInput("n1",
                        "Number", value=0),
            selectInput("operator","Operator",c("+","-","/","*")),
            numericInput("n2",
                         "Number", value=0)
        ),

        # Show a plot of the generated distribution
        mainPanel(
           shiny::h2("Result:"),
           htmlOutput("result")
        )
    )
)

# Define server logic required to draw a histogram
server <- function(input, output) {

    output$result <- renderText({
       result <- switch (input$operator,
          "+" = input$n1+input$n2,
          "-" = input$n1-input$n2,
          "/" = input$n1/input$n2,
          "*" = input$n1*input$n2
        )
       
       result <- paste0("<h2>",result,"</h2>")
       
        return(result)
    })
}

# Run the application 
shinyApp(ui = ui, server = server)

Who doesn’t like penguins?

Palmer Penguins

We are going to use the penguins dataset from palmerpenguins

species island bill_length_mm bill_depth_mm flipper_length_mm body_mass_g sex year
Adelie Torgersen 39.1 18.7 181 3750 male 2007
Adelie Torgersen 39.5 17.4 186 3800 female 2007
Adelie Torgersen 40.3 18.0 195 3250 female 2007
Adelie Torgersen NA NA NA NA NA 2007
Adelie Torgersen 36.7 19.3 193 3450 female 2007
Adelie Torgersen 39.3 20.6 190 3650 male 2007

Reactive expression

R/challenge2.R

library(shiny)
library(tidyverse)
library(palmerpenguins)

# Define UI for application that draws a histogram
ui <- fluidPage(

    # Application title
    titlePanel("Penguins"),

    # Sidebar with a slider input for number of bins 
    sidebarLayout(
        sidebarPanel(
            # <-------   here go input elements
        ),

        # Show a plot of the generated distribution
        mainPanel(
           plotOutput("plot1"),
           plotOutput("plot2"),
           textOutput("text1")
           # <-------    add more outputs here if needed
        )
    )
)

# Define server logic required to draw a histogram
server <- function(input, output) {

    output$plot1 <- renderPlot({
       penguins %>% ggplot(aes(x=body_mass_g,y=bill_length_mm))+
        geom_point()+
        geom_smooth(method = "lm")
    })
    
    output$plot2 <- renderPlot({
      # <------ generate plot here (ggplot, or base R)
    })
    
    output$text1 <- renderText({
      # <------- generate some text here
    })
}

# Run the application 
shinyApp(ui = ui, server = server)

Your Turn - Exercise 3

  • Copy the code from the previous slide (or open R/challenge2.R) and run it in R

  • Add logic to create a second plot as output plot2 on the server

  • Add extra inputs (e.g., add a selectInput for subgroup selection of penguin species) or add a rangeInput to display only certain ranges of years, or make point size adjustable by a given variable (selectInput or a checkboxInput).

DRY - Don’t repeat yourself

  • Assume a range input (sliderInput(value=c(0,10))) that filters data

  • Filter logic should be executed only once for every relevant output

  • Never copy&paste server logic, instead use a reactive element

DRY - Don’t repeat yourself

flowchart LR
  subgraph outputs
  pl1([plot1])
  pl2([plot2])
  tx1([text1])
  end
  compute([compute])
  subgraph inputs
  slider1([slider1]) 
  n1([number1]) 
  rn1([range1])
  ck1([ck1])
  end
  compute --> pl1
  compute --> pl2
  rn1 --> compute
  n1 --> pl1

Reactives

Their primary use is similar to a function in an R script, they help to

  • avoid repeating yourself

  • decompose complex computations into smaller / more modular steps

  • can improve computational efficiency by breaking up / simplifying reactive dependencies

DRY - Solution

R/demo3.R

library(shiny)
library(tidyverse)
library(palmerpenguins)

# Define UI for application that draws a histogram
ui <- fluidPage(

    # Application title
    titlePanel("Penguins"),

    # Sidebar with a slider input for number of bins 
    sidebarLayout(
        sidebarPanel(
            sliderInput("rng", "Range ",value=c(3000,5000),min=2700, max=6300),
            selectInput("size", label="Size", choices=c("flipper_length_mm","bill_length_mm")),
            checkboxInput("grp",label="Subgroups", value=TRUE)
        ),

        # Show a plot of the generated distribution
        mainPanel(
           plotOutput("plot1"),
           plotOutput("plot2"),
           textOutput("text1")
        )
    )
)

# Define server logic required to draw a histogram
server <- function(input, output) {

    penguins_filtered <- reactive({
      penguins %>% filter(body_mass_g >= input$rng[1] & body_mass_g <= input$rng[2])
    })
  
    output$plot1 <- renderPlot({
      
      wf <- NULL
      if (input$grp) {
        wf <- facet_wrap(~species)
      }
      
       penguins_filtered() %>% ggplot(aes(x=body_mass_g,y=bill_length_mm))+
        geom_point(aes_string(size=input$size))+
        geom_smooth(method = "lm")+
        wf
    })
    
    output$plot2 <- renderPlot({
      if (input$grp) {
        penguins_filtered() %>% ggplot(aes(x=flipper_length_mm,fill=species))+geom_histogram()
      } else {
        penguins_filtered() %>% ggplot(aes(x=flipper_length_mm))+geom_histogram()
      }

    })
    
    output$text1 <- renderText({
      paste0("<b>There</b> are ",nrow(penguins_filtered()), " penguins in the data set")
    })
}

# Run the application 
shinyApp(ui = ui, server = server)

Deployment

  • Free online deployment at https://www.shinyapps.io/ after registration
  • Free account limited (e.g., 25h operating hours, 5 apps; more plans available)
  • Sharing your app for others to run it locally (e.g., via OSF)
  • Reproducibility! Make sure that everything is contained, no absolute file paths were used (see here package) and that all dependencies are loaded

Dashboards

Package shinydashboard has some nice GUI elements for dashboards:

Demo Dashboard

R/demo7.R

library(shinydashboard)

ui <- dashboardPage(
  dashboardHeader(title = "Value boxes"),
  dashboardSidebar(),
  dashboardBody(
    fluidRow(
      # A static valueBox
      valueBox(20, "New Orders", icon = icon("credit-card")),
      
      # Dynamic valueBox
      valueBoxOutput("progressBox"),
      
    ),
    fluidRow(
      # Clicking this will increment the progress amount
      box(width = 4, actionButton("count", "Do some work"))
    )
  )
)

server <- function(input, output) {
  output$progressBox <- renderValueBox({
    
    if (input$count < 10) {
      ic <- icon("thumbs-down")
      col <- "red"
    } else {
      ic <- icon("thumbs-up") 
      col <- "green"
    }
    
    valueBox(
      paste0(input$count, "%"), "Progress", icon = ic,
      color = col
    )
  })
  

}

shinyApp(ui, server)

Simulation

Shiny is useful for simulating data (multivariate distributions, network graphs, agents, …)

  • Inputs allow us to vary simulation parameters
  • Outputs display simulation results
  • We use a reactive() to generate our dataset, so that it can be reused in different places
  • downloadButton and downloadHandler allow us to download the simulated data files for later analyses

Simulation Stub

R/demo6.R

library(shiny)

# Define UI for application that draws a histogram
ui <- fluidPage(

    # Application title
    titlePanel("Simulation"),

    # Sidebar with a slider input for number of bins 
    sidebarLayout(
        sidebarPanel(
            numericInput("N",
                        "Sample Size", value=100),
            downloadButton("download")
            
        ),

        # Show a plot of the generated distribution
        mainPanel(
           plotOutput("graph")
        )
    )
)

# Define server logic 
server <- function(input, output) {

  sim <- reactive({
   # <----- create a simulated dataset here
  })
  
  # return the dataset as file
  output$download = downloadHandler(
    filename = function() {
      "simulation.csv"
    },
    content = function(file) {
      readr::write_csv(sim(), file)
    }
  )
  
    output$graph <- renderPlot({
      
      # <------ do some plotting here
      
    })
    
    
}

# Run the application 
shinyApp(ui = ui, server = server)

Your Turn - Exercise 4

Copy the code from the previous slide (or open R/demo6.R) and run it in R

  • Add logic to simulate data (e.g., using rnorm or MASS::mvrnorm)
  • Add a plot to show the simulation results (e.g., a scatterplot)
  • Add extra features to make the simulation interactive

Simulation Solution

R/solution6.R

library(shiny)

# Define UI for application
ui <- fluidPage(

    # Application title
    titlePanel("Simulation"),

    # Sidebar with a slider input for number of bins 
    sidebarLayout(
        sidebarPanel(
            numericInput("N",
                        "Sample Size", value=100),
            numericInput("r",
                         "Correlation", value=0),
            downloadButton("download")
            
        ),

        # Show a plot of the generated distribution
        mainPanel(
           plotOutput("graph")
        )
    )
)

# Define server logic 
server <- function(input, output) {

  sim <- reactive({
    r = input$r
    N = input$N
    
    df <- MASS::mvrnorm(n=N, mu=c(0,0),
                        Sigma=matrix(c(1,r,
                                       r,1),
                                     nrow=2))
    
    df <- data.frame(df)
    names(df) <- c("x","y")
    
    return(df)
  })
  
  output$download = downloadHandler(
    filename = function() {
      "simulation.csv"
    },
    content = function(file) {
      readr::write_csv(sim(), file)
    }
  )
  
    output$graph <- renderPlot({
      
       sim() %>% ggplot(aes(x=x,y=y))+ geom_point()+geom_smooth(method = "lm")
      
    })
    
    
}

# Run the application 
shinyApp(ui = ui, server = server)

Inspiration

The Shiny User Showcase is comprised of contributions from the Shiny app developer community.

Your turn - go wild!

License

To the extent possible under law and unless otherwise noted, Andreas and Leonie have waived all copyright and related or neighboring rights to this workshop document and the accompanying R source codes. This work is published from: Deutschland/Germany.

Some parts of this workshop are inspired by work by Colin Rundel (https://github.com/rstudio-conf-2022/get-started-shiny/), which is provided under https://creativecommons.org/licenses/by/4.0/.

Illustrations by undraw https://undraw.co (see their license https://undraw.co/license)

Thanks

Thank you for being on this journey with us!

Andreas (https://www.brandmaier.de; also find me on Twitter), Bluesky, Linkedin)